<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <h1 data-lake-id="dn3Ux" id="dn3Ux"><strong><span data-lake-id="uef78ee40" id="uef78ee40" class="lake-fontsize-24" style="color: rgb(38, 38, 38)">​</span></strong><span data-lake-id="u3b7192d9" id="u3b7192d9">典型回答</span></h1>
  <p data-lake-id="u07589b9c" id="u07589b9c"><span data-lake-id="uaa86906f" id="uaa86906f">无论是Mybatis也好，Spring也罢，它们的执行过程无非可分为启动阶段和运行阶段：</span></p>
  <p data-lake-id="uea98229c" id="uea98229c"><span data-lake-id="ucbff254b" id="ucbff254b">启动阶段：</span></p>
  <ol list="u3798697c">
   <li fid="u0760686b" data-lake-id="u9cf1ec77" id="u9cf1ec77"><span data-lake-id="uef3c0b74" id="uef3c0b74">定义配置文件，如XML，注解</span></li>
   <li fid="u0760686b" data-lake-id="u2bdaaa5d" id="u2bdaaa5d"><span data-lake-id="u2754aeff" id="u2754aeff">解析配置文件，将配置文件加载到内存当中</span></li>
  </ol>
  <p data-lake-id="udb204cb8" id="udb204cb8"><span data-lake-id="uacee1167" id="uacee1167">​</span><br></p>
  <p data-lake-id="u898c5fd6" id="u898c5fd6"><span data-lake-id="uc1937dec" id="uc1937dec">运行阶段：</span></p>
  <ol list="ud04fb449">
   <li fid="ufd7d0746" data-lake-id="u1ac6fc12" id="u1ac6fc12"><span data-lake-id="u22c18cd6" id="u22c18cd6">读取内存中的配置文件，并根据配置文件实现对应的功能</span></li>
  </ol>
  <p data-lake-id="ub0fc9ade" id="ub0fc9ade"><br></p>
  <p data-lake-id="u0d9b6316" id="u0d9b6316"><span data-lake-id="u6a6cb40b" id="u6a6cb40b">对于执行SQL的逻辑来讲，有如下步骤：</span></p>
  <p data-lake-id="u54c099c4" id="u54c099c4"><span data-lake-id="ub9194a1d" id="ub9194a1d">​</span><br></p>
  <p data-lake-id="u4d27e856" id="u4d27e856"><span data-lake-id="u8fbc16c0" id="u8fbc16c0">当配置完成之后，假如说我们要执行一个下面一个sql，那么该如何执行呢？</span></p>
  <pre lang="java"><code>
TestMapper testMapper = session.getMapper(TestMapper.class);
Test test = testMapper.findOne(1);
</code></pre>
  <h3 data-lake-id="x0889" id="x0889"><span data-lake-id="u8a3ed647" id="u8a3ed647">代理类的生成</span></h3>
  <p data-lake-id="udf50ca8d" id="udf50ca8d"><span data-lake-id="u710e3378" id="u710e3378">首先Mybatis会根据我们传入接口通过JDK动态代理，生成一个代理对象TestMapper，生成逻辑如下所示：</span></p>
  <pre lang="java"><code>
public T newInstance(SqlSession sqlSession) {
    // mapperProxy实现了Invocationhandler接口，用于JDK动态代理
    final MapperProxy&lt;T&gt; mapperProxy = new MapperProxy&lt;&gt;(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
}
// 通过JDK动态代理生成对象
protected T newInstance(MapperProxy&lt;T&gt; mapperProxy) {
	return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
</code></pre>
  <p data-lake-id="uff3abac1" id="uff3abac1"><span data-lake-id="u29ec6678" id="u29ec6678">代理类的主要逻辑在MapperProxy中，而代理逻辑则是通过MapperMethod完成的。</span></p>
  <p data-lake-id="u0e22e33b" id="u0e22e33b"><span data-lake-id="u816196ce" id="u816196ce">对于MapperMethod来说，它在创建的时候是需要读取XML或者方法注解的配置项，所以在使用的时候才能知道具体代理的方法的SQL内容。同时，这个类也会解析和记录被代理方法的入参和出参，以方便对SQL的查询占位符进行替换，同时对查询到的SQL结果进行转换。</span></p>
  <h3 data-lake-id="FeKxX" id="FeKxX"><span data-lake-id="uedf5fc9c" id="uedf5fc9c">执行SQL</span></h3>
  <p data-lake-id="ub949733c" id="ub949733c"><span data-lake-id="u6572c803" id="u6572c803">代理类生成之后，就可以执行代理类的具体逻辑，也就是真正开始执行用户自定义的SQL逻辑了。</span></p>
  <p data-lake-id="uf42b535e" id="uf42b535e"><span data-lake-id="u5a1b73aa" id="u5a1b73aa">首先会进入到MapperMethod核心的执行逻辑，如下所示：</span></p>
  <pre lang="java"><code>
public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      case INSERT: {
      Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.insert(command.getName(), param));
        break;
      }
      case UPDATE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.update(command.getName(), param));
        break;
      }
      case DELETE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.delete(command.getName(), param));
        break;
      }
      case SELECT:
        if (method.returnsVoid() &amp;&amp; method.hasResultHandler()) {
          executeWithResultHandler(sqlSession, args);
          result = null;
        } else if (method.returnsMany()) {
          result = executeForMany(sqlSession, args);
        } else if (method.returnsMap()) {
          result = executeForMap(sqlSession, args);
        } else if (method.returnsCursor()) {
          result = executeForCursor(sqlSession, args);
        } else {
          Object param = method.convertArgsToSqlCommandParam(args);
          result = sqlSession.selectOne(command.getName(), param);
        }
        break;
      case FLUSH:
        result = sqlSession.flushStatements();
        break;
      default:
        throw new BindingException("Unknown execution method for: " + command.getName());
    }
     // ...
    return result;
  }
</code></pre>
  <p data-lake-id="udad69fcf" id="udad69fcf"><span data-lake-id="u2c12406b" id="u2c12406b">通过代码我们可以很清晰的发现，为什么Mybatis的insert，update和delete会返回行数的原因。业务处理上，我们经常通过update==1来判断当前语句是否更新成功。</span></p>
  <p data-lake-id="u21776c60" id="u21776c60"><span data-lake-id="u40ee89ba" id="u40ee89ba">这里一共做了两件事情，一件事情是通过BoundSql将方法的入参转换为SQL需要的入参形式，第二件事情就是通过SqlSession来执行对应的Sql。下面我们通过select来举例。</span></p>
  <h3 data-lake-id="ChWfe" id="ChWfe"><span data-lake-id="ubb5a2317" id="ubb5a2317">缓存</span></h3>
  <p data-lake-id="u0f24f503" id="u0f24f503"><span data-lake-id="u753772d8" id="u753772d8">Sqlsession是Mybatis对Sql执行的封装，真正的SQL处理逻辑要通过Executor来执行。Executor有多个实现类，因为在查询之前，要先check缓存是否存在，所以默认使用的是CachingExecutor类，顾名思义，它的作用就是二级缓存。</span></p>
  <p data-lake-id="uc43409eb" id="uc43409eb"><img src="https://cdn.nlark.com/yuque/0/2023/jpeg/719664/1675262096615-980676ef-ddc6-4a9c-b9e9-f6157b4d59bc.jpeg?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_45%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"></p>
  <p data-lake-id="u04a24163" id="u04a24163"><span data-lake-id="u31c0d618" id="u31c0d618">CachingExecutor的执行逻辑如下所示：</span></p>
  <pre lang="java"><code>
public &lt;E&gt; List&lt;E&gt; query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    Cache cache = ms.getCache();
    if (cache != null) {
      flushCacheIfRequired(ms);
      if (ms.isUseCache() &amp;&amp; resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        List&lt;E&gt; list = (List&lt;E&gt;) tcm.getObject(cache, key);
        if (list == null) {
          list = delegate.&lt;E&gt; query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          // 放缓存
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    // 若二级缓存为空，则重新查询数据库
    return delegate.&lt;E&gt; query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }
</code></pre>
  <p data-lake-id="u34345ae4" id="u34345ae4"><span data-lake-id="u79986e64" id="u79986e64">二级缓存是和命名空间绑定的，如果多表操作的SQL的话，是会出现脏数据的。同时如果是不同的事务，也可能引起脏读，所以要慎重。</span></p>
  <p data-lake-id="ua5764927" id="ua5764927"><span data-lake-id="u09a467ef" id="u09a467ef">如果二级缓存没有命中则会进入到BaseExecutor中继续执行，在这个过程中，会调用一级缓存执行。</span></p>
  <p data-lake-id="u333a7920" id="u333a7920"><span data-lake-id="uf790ff20" id="uf790ff20">值得一提的是，在Mybatis中，缓存分为PerpetualCache, BlockingCache, LruCache等，这些cache的实现则是借用了装饰者模式。一级缓存使用的是PerpetualCache，里面是一个简单的HashMap。一级缓存会在更新的时候，事务提交或者回滚的时候被清空。换句话说，一级缓存是和SqlSession绑定的。</span></p>
  <p data-lake-id="uc21ba5cb" id="uc21ba5cb"><span data-lake-id="udd60f55c" id="udd60f55c">具体的细节可以参考：</span></p>
  <h3 data-lake-id="PNWGM" id="PNWGM"><span data-lake-id="u37d0f072" id="u37d0f072">查询数据库</span></h3>
  <p data-lake-id="uabe4fa18" id="uabe4fa18"><span data-lake-id="ubd273b25" id="ubd273b25">如果一级缓存中没有的话，则需要调用JDBC执行真正的SQL逻辑。我们知道，在调用JDBC之前，是需要建立连接的，如下代码所示：</span></p>
  <pre lang="java"><code>
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    Connection connection = getConnection(statementLog);
    stmt = handler.prepare(connection, transaction.getTimeout());
    handler.parameterize(stmt);
    return stmt;
}
</code></pre>
  <p data-lake-id="u7e5172ed" id="u7e5172ed"><span data-lake-id="uc0ae542c" id="uc0ae542c">我们会发现，Mybatis并不是直接从JDBC获取连接的，通过数据源来获取的，Mybatis默认提供了是那种种数据源：JNDI，PooledDataSource和UnpooledDataSource，我们也可以引入第三方数据源，如Druid等。包括驱动等都是通过数据源获取的。</span></p>
  <p data-lake-id="ue26d9e3f" id="ue26d9e3f"><span data-lake-id="u612f7422" id="u612f7422">获取到Connection之后，还不够，因为JDBC的数据库操作是需要Statement的，所以Mybatis专门抽象出来了</span><code data-lake-id="u1ac66963" id="u1ac66963"><span data-lake-id="u70af2a9c" id="u70af2a9c">StatementHandler</span></code><span data-lake-id="u31a33a55" id="u31a33a55">处理类来专门处理和JDBC的交互，如下所示：</span></p>
  <pre lang="java"><code>
public &lt;E&gt; List&lt;E&gt; query(Statement statement, ResultHandler resultHandler) throws SQLException {
    String sql = boundSql.getSql();
    statement.execute(sql);
    return resultSetHandler.&lt;E&gt;handleResultSets(statement);
  }
</code></pre>
  <p data-lake-id="u6f922023" id="u6f922023"><span data-lake-id="ufa93d9fd" id="ufa93d9fd">其实这三行代码就代表了Mybatis执行SQL的核心逻辑：组装SQL，执行SQL，组装结果。仅此而已。</span></p>
  <p data-lake-id="ua5ff1cc5" id="ua5ff1cc5"><span data-lake-id="u0da464a2" id="u0da464a2">具体Sql是如何组装的呢？是通过BoundSql来完成的，具体组装的逻辑大家可以从</span><code data-lake-id="ufbfc2c87" id="ufbfc2c87"><span data-lake-id="ufbc5ef8d" id="ufbc5ef8d">org.apache.ibatis.mapping.MappedStatement#getBoundSql</span></code><span data-lake-id="u46a14e70" id="u46a14e70">中了解，这里不再赘述。</span></p>
  <h3 data-lake-id="BBVfN" id="BBVfN"><span data-lake-id="u0a87005e" id="u0a87005e">处理查询结果</span></h3>
  <p data-lake-id="ua3c2e877" id="ua3c2e877"><span data-lake-id="u7fa345d3" id="u7fa345d3">当我们获取到查询结果之后，就需要对查询结果进行封装，即把查询到的数据库字段映射为DO对象。</span></p>
  <p data-lake-id="ufe51b20b" id="ufe51b20b"><span data-lake-id="ub69286a8" id="ub69286a8">因为此时我们已经拿到了执行结果ResultSet，同时我们也在应用启动的时候在配置文件中配置了DO到数据库字段的映射ResultMap，所以通过这两个配置就可以转换。核心的转换逻辑是通过TypeHandler完成的，流程如下所示：</span></p>
  <ol list="u0cd1050d">
   <li fid="ufee23345" data-lake-id="u1c4f935a" id="u1c4f935a"><span data-lake-id="u62b652aa" id="u62b652aa">创建返回的实体类对象，如果该类是延迟加载，则先生成代理类</span></li>
   <li fid="ufee23345" data-lake-id="uf1f96023" id="uf1f96023"><span data-lake-id="uc89a6cc5" id="uc89a6cc5">根据ResultMap中配置的数据库字段，将该字段从ResultSet取出来</span></li>
   <li fid="ufee23345" data-lake-id="ufea65329" id="ufea65329"><span data-lake-id="ud4770a87" id="ud4770a87">从ResultMap中获取映射关系，如果没有，则默认将下划线转为驼峰式命名来映射</span></li>
   <li fid="ufee23345" data-lake-id="u1635a1e1" id="u1635a1e1"><span data-lake-id="u9b78bde7" id="u9b78bde7">通过setter方法反射调用，将数据库的值设置到实体类对象当中</span></li>
  </ol>
 </body>
</html>